package gggameserver.games.khet;

import gggameserver.games.Board;
import gggameserver.games.Game;
import gggameserver.games.Move;
import gggameserver.games.Position;
import gggameserver.games.khet.KhetCommandFormatter.GameStatus;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class KhetBoard extends Board {

	public enum GameEndReasonDetails {
		NORMAL, MOVE_LIMIT_EXCEEDED, SITUATION_3D
	};

	public static final int BOARD_WIDTH = 10;
	public static final int BOARD_HEIGHT = 8;

	public static final int PLAYER_EMPTY = -1;
	public static final int PLAYER_SILVER = PLAYER_FIRST;
	public static final int PLAYER_RED = PLAYER_FIRST + 1;

	public static final int MOVE_LIMIT = 222;

	/** silver Ankh/red Eye of Horus locations */
	public int[][] special; // TODO
	/** list of pieces per player */
	public ArrayList<KhetPiece>[] pieces; // TODO
	/** Placement of pieces */
	public KhetPiece[][] board; // TODO

	private int movesDone = 0;

	private KhetMove lastMove = null;
	private KhetShotResult lastShot = null;

	private GameEndReasonDetails endReasonDetails = GameEndReasonDetails.NORMAL;
	private Map<String, Integer> moveHistory = new HashMap<String, Integer>(
			MOVE_LIMIT, 0.5f);

	public KhetBoard() {
		super(2);
		initClassicBoard();
	}

	private KhetBoard(ArrayList<KhetPiece> pieces, int playerNumber) {
		super(2);
		this.initEmptyBoard();

		this.moveToPlayer(playerNumber);
		for (KhetPiece piece : pieces) {
			this.pieces[piece.getPlayerNumber() - 1].add(piece);// TODO HACK
																// that -1
																// thingy
		}

		// synchronize list...
		this.populateBoard();
	}

	public static KhetBoard createBoard(ArrayList<KhetPiece> pieces,
			int playerNumber, GameStatus status,
			Map.Entry<KhetMove, KhetShotResult> lastMove) {
		KhetBoard board = new KhetBoard(pieces, playerNumber);

		// TODO status

		if (null != lastMove) {
			board.lastMove = lastMove.getKey();
			board.lastShot = lastMove.getValue();
		}
		return board;
	}

	public GameEndReasonDetails getGameEndReasonDetails() {
		return this.endReasonDetails;
	}

	public void setGameEndReasonDetails(GameEndReasonDetails reason) {
		if (null == reason)
			throw new IllegalStateException("Reason is null");
		this.endReasonDetails = reason;
	}

	@Override
	public synchronized void makeMove(Move m) {
		KhetMove khetMove = (KhetMove) m;
		KhetMoveResult result = getMoveResult(khetMove);

		if (!result.isLegal())
			throw new IllegalStateException("Illegal move " + m);

		Position oldPosition = khetMove.getPostion();
		Position newPosition = result.getNewPosition();

		// move piece
		KhetPiece piece = getPieceAt(oldPosition);
		piece.setDirection(result.getNewDirection());
		piece.setPosition(newPosition);

		// check if swap place with another piece
		KhetPiece piece2 = getPieceAt(newPosition);

		if (null != piece2 && piece != piece2) {
			piece2.setPosition(oldPosition);
		}

		// ... prepare board for (calculating) destruction
		populateBoard();

		// check if somebody is destroyed
		KhetShotResult shotResult = getShotResult(m.getPlayerNumber());
		Position shotTarget = shotResult.getDestroyedPosition();
		if (null != shotTarget) {
			KhetPiece targetPiece = getPieceAt(shotTarget);
			targetPiece.setDestroyed();
			populateBoard();
		}

		// last move
		lastMove = khetMove; // TODO clone
		lastShot = shotResult;

		// check for the end condition
		boolean silverAlive = isPharaonAlive(pieces[0]);
		boolean redAlive = isPharaonAlive(pieces[1]);

		if (!silverAlive && !redAlive)
			setDraw();
		else if (!silverAlive && redAlive)
			setWinner(PLAYER_RED);
		else if (silverAlive && !redAlive)
			setWinner(PLAYER_SILVER);

		// switch player
		moveToNextPlayer();
		++movesDone;

		// checking for end condition...
		
		// data for 3D rule
		String boardStr = KhetCommandFormatter.getPiecesAsString(this);
		
		// ... update occurrence cnt
		Integer boardStateOccurrence = moveHistory.get(boardStr);
		if (null == boardStateOccurrence) 
		    boardStateOccurrence = 0;
		boardStateOccurrence = boardStateOccurrence + 1;
		moveHistory.put(boardStr, boardStateOccurrence);

		if (isGameOver())
		    return;
		
		// special draw situations
		if (movesDone >= MOVE_LIMIT) {
			this.setDraw();
			this.setGameEndReasonDetails(GameEndReasonDetails.MOVE_LIMIT_EXCEEDED);
		} else if (boardStateOccurrence.intValue() >= 3) {
            this.setDraw();
            this.setGameEndReasonDetails(GameEndReasonDetails.SITUATION_3D);
		}
	}

	public int getMovesDone() {
		return this.movesDone;
	}

	public final KhetMove getLastMove() {
		return this.lastMove;
	}

	public final KhetShotResult getLastShot() {
		return this.lastShot;
	}

	@Override
	protected Move commandToMoveSimple(String command) throws Exception {
		return KhetCommandReader.getMove(command);
	}

	@Override
	public String moveToCommand(Move m) {
		KhetMove move = (KhetMove) m;
		String str = KhetCommandFormatter.getMoveCommandAsString(move);
		return str;
	}

	@Override
	public boolean isLeagal(Move move) {
		return getMoveResult((KhetMove) move).isLegal();
	}

	// ----------------------------------------------------------------
	private KhetMoveResult getMoveResult(KhetMove move) {
		if (isGameOver())
			return new KhetMoveResult("game is over");

		if (getActivePlayerNumber() != move.getPlayerNumber())
			return new KhetMoveResult("wrong player");

		Position oldPos = new Position(move.getX(), move.getY());

		// find piece...
		if (!isOnBoard(oldPos))
			return new KhetMoveResult("illegal piece location (" + move + ")");

		KhetPiece piece = this.getPieceAt(oldPos);

		if (null == piece)
			return new KhetMoveResult("no piece at (" + move + ")");

		if (move.getPlayerNumber() != piece.getPlayerNumber())
			return new KhetMoveResult("piece at (" + move
					+ ") doesn't belong to player");

		// piece found...
		int oldDirection = piece.getDirection();

		// calculate new position...
		int newDirection = oldDirection;
		Position newPos = new Position(oldPos);

		if (KhetMove.ACTION_TURN_CLOCKWISE == move.getAction()) {
			// rotating clockwise
			newDirection = clockwiseDirection(oldDirection);
		} else if (KhetMove.ACTION_TURN_COUNTERCLOCKWISE == move.getAction()) {
			// rotating counterclockwise
			newDirection = counterclockwiseDirection(oldDirection);
		} else {
			// move piece
			if (piece.getType() == KhetPiece.Type.SPHINX)
				return new KhetMoveResult("sphinx can't move");

			// find direction
			Position dmove = calculateDMove(move.getAction());
			if (null == dmove)
				return new KhetMoveResult("illecal action " + move.getAction());

			// actual moving
			newPos.add(dmove);

			// check new position...
			// ...is on board
			if (!isOnBoard(newPos))
				return new KhetMoveResult("can't move off board ("
						+ newPos.getX() + ";" + newPos.getY() + ")");

			// ...is allowed to move
			if (!this.isPlaceAllowed(piece, newPos))
				return new KhetMoveResult("can't move to special position ("
						+ newPos.getX() + ";" + newPos.getY() + ")");

			// ...is already occupied
			KhetPiece destPiece = this.board[newPos.getX()][newPos.getY()];
			if (null != destPiece) {
				// scarab can swap places with pyramid or anubis of either color
				if (piece.getType() == KhetPiece.Type.SCARAB
						&& (destPiece.getType() == KhetPiece.Type.PYRAMID || destPiece
								.getType() == KhetPiece.Type.ANUBIS)) {
					if (!this.isPlaceAllowed(destPiece, oldPos))
						return new KhetMoveResult(
								"can't move to special position ("
										+ newPos.getX() + ";" + newPos.getY()
										+ ")");
				} else {
					return new KhetMoveResult(
							"can't move to square occupied by other piece ("
									+ newPos.getX() + ";" + newPos.getY() + ")");
				}
			} // null != destPiece
		}

		// check sphinx direction
		if (piece.getType() == KhetPiece.Type.SPHINX) {
			Position sphinxTarget = new Position(newPos);
			sphinxTarget.add(getDirectionDMove(newDirection));

			if (!isOnBoard(sphinxTarget))
				return new KhetMoveResult("wrong sphinx direction "
						+ newDirection);
		}

		return new KhetMoveResult(newPos, newDirection);
	}

	private boolean isPlaceAllowed(KhetPiece piece, Position pos) {
		int posOwner = this.special[pos.getX()][pos.getY()];
		return PLAYER_EMPTY == posOwner || piece.getPlayerNumber() == posOwner;
	}

	public KhetPiece getPieceAt(Position oldPosition) {
		return board[oldPosition.getX()][oldPosition.getY()];
	}

	public KhetPiece getPieceAt(int x, int y) {
		return board[x][y];
	}

	private KhetShotResult getShotResult(int player) {
		// find sphinx...
		ArrayList<KhetPiece> playerPieces = pieces[player - PLAYER_FIRST];

		KhetPiece sphinx = null;
		for (KhetPiece piece : playerPieces)
			if (piece.getType() == KhetPiece.Type.SPHINX) {
				sphinx = piece;
				break;
			}

		if (null == sphinx)
			throw new IllegalStateException("can't find sphinx");

		// find shot path...
		KhetPiece destroyedPiece = null;
		int shotDirection = sphinx.getDirection();

		ArrayList<Position> trajectory = new ArrayList<Position>();
		trajectory.add(new Position(sphinx.getPosition()));

		while (true) {
			// get current position and direction
			Position pos = trajectory.get(trajectory.size() - 1);
			Position dmove = getDirectionDMove(shotDirection);

			// calculate new position
			Position newPos = new Position(pos).add(dmove);
			trajectory.add(newPos);

			// calculate result...
			// ... out of board - break
			if (!isOnBoard(newPos))
				break;

			// ... hit a piece?
			KhetPiece piece = this.board[newPos.getX()][newPos.getY()];
			// ... if no, then continue ...
			if (null == piece)
				continue;

			// ... hit detected
			if (KhetPiece.Type.SCARAB == piece.getType()) {
				// SCARAB
				shotDirection = getScarabReflection(piece, shotDirection);
			} else if (KhetPiece.Type.PYRAMID == piece.getType()) {
				// PYRAMID
				shotDirection = getPyramidReflection(piece, shotDirection);
				// ... if pyramid is hit
				if (shotDirection < 0) {
					destroyedPiece = piece;
					break;
				}
			} else if (KhetPiece.Type.PHARAON == piece.getType()) {
				// PHARAON - always a kill
				destroyedPiece = piece;
				break;
			} else if (KhetPiece.Type.ANUBIS == piece.getType()) {
				// ANUBIS - hit, check if kill
				if (isAnubisKilled(piece, shotDirection))
					destroyedPiece = piece;
				break;
			} else if (KhetPiece.Type.SPHINX == piece.getType()) {
				// SPHINX - hit, no kill
				break;
			} else {
				throw new IllegalStateException("Illegal type "
						+ piece.getType());
			}
		} // while

		// shot result
		return new KhetShotResult(trajectory, destroyedPiece);
	}

	private boolean isPharaonAlive(ArrayList<KhetPiece> p) {
		for (KhetPiece piece : p)
			if (piece.isAlive() && piece.getType() == KhetPiece.Type.PHARAON)
				return true;
		return false;
	}

	private boolean isAnubisKilled(KhetPiece piece, int shotDirection) {
		// piece direction...
		int dir = piece.getDirection();
		// safe shot direction...
		int safeShotDir = mirrorDirection(dir);

		// killed if shot was not from safe direction
		return safeShotDir != shotDirection;
	}

	private int getPyramidReflection(KhetPiece pyramid, int shotDirection) {
		int dir = pyramid.getDirection();
		return getPyramidReflection(dir, shotDirection);
	}

	private int getScarabReflection(KhetPiece scarab, int shotDirection) {
		int dir = scarab.getDirection();
		int dir_m = mirrorDirection(dir);

		// emulate pyramide...
		int newDir = getPyramidReflection(dir, shotDirection);
		// ... if one direction didn't fit, then rotate pyramid ...
		if (newDir < 0)
			newDir = getPyramidReflection(dir_m, shotDirection);

		return newDir;
	}

	private int getPyramidReflection(int dir, int shotDirection) {
		// assume dir is up and Pyramid looks like .\
		int dir_90 = clockwiseDirection(dir);
		int dir_180 = clockwiseDirection(dir_90);
		int dir_270 = clockwiseDirection(dir_180);

		// shot from north (to south) goes to east
		if (shotDirection == dir_180)
			return dir_90;
		// shot from east (to west) goes to north
		if (shotDirection == dir_270)
			return dir;

		// no reflection
		return -1;
	}

	public static Position getDirectionDMove(int direction) {
		switch (direction) {
		case KhetPiece.NORTH:
			return new Position(0, -1);
		case KhetPiece.SOUTH:
			return new Position(0, 1);
		case KhetPiece.EAST:
			return new Position(1, 0);
		case KhetPiece.WEST:
			return new Position(-1, 0);
		default:
			throw new IllegalStateException("Illegal direction " + direction);
		}
	}

	public Position calculateDMove(int action) {
		if (0 <= action && action <= 8 && 4 != action) {
			return new Position((action % 3) - 1, (action / 3) - 1);
		}
		return null;
	}

	public boolean isOnBoard(Position pos) {
		return 0 <= pos.getX() && pos.getX() < BOARD_WIDTH && 0 <= pos.getY()
				&& pos.getY() < BOARD_HEIGHT;
	}

	// ----------------------------------------------------------------
	/**
	 * Initializes board with classic initial setup
	 */
	private void initClassicBoard() {
		initEmptyBoard();

		// silver
		ArrayList<KhetPiece> silver = this.pieces[0];
		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.SPHINX, 9, 7,
				KhetPiece.NORTH));

		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.PYRAMID, 2, 7,
				KhetPiece.NORTH_WEST));
		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.PYRAMID, 7, 6,
				KhetPiece.NORTH_EAST));
		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.PYRAMID, 9, 4,
				KhetPiece.SOUTH_WEST));
		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.PYRAMID, 2, 4,
				KhetPiece.NORTH_WEST));
		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.PYRAMID, 9, 3,
				KhetPiece.NORTH_WEST));
		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.PYRAMID, 2, 3,
				KhetPiece.SOUTH_WEST));
		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.PYRAMID, 3, 2,
				KhetPiece.NORTH_WEST));

		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.ANUBIS, 3, 7,
				KhetPiece.NORTH));
		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.ANUBIS, 5, 7,
				KhetPiece.NORTH));

		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.SCARAB, 4, 4,
				KhetPiece.NORTH_WEST));
		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.SCARAB, 5, 4,
				KhetPiece.SOUTH_WEST));

		silver.add(new KhetPiece(PLAYER_SILVER, KhetPiece.Type.PHARAON, 4, 7,
				KhetPiece.NORTH));

		// red
		ArrayList<KhetPiece> red = this.pieces[1];
		mirrorPieces(silver, red);

		populateBoard();
	}

	/**
	 * Puts all the pieces on proper places on board
	 */
	private void populateBoard() {
		// clean board
		for (int i = 0; i < board.length; ++i)
			for (int j = 0; j < board[i].length; ++j)
				board[i][j] = null;
		// put pieces on board
		for (int player = 0; player < 2; ++player)
			for (KhetPiece piece : pieces[player])
				if (null != piece && piece.isAlive()) {
					int x = piece.getX();
					int y = piece.getY();
					if (null != board[x][y])
						throw new IllegalStateException("Multiple pieces in x="
								+ x + " y=" + y);
					board[x][y] = piece;
				} //
	}

	/**
	 * Initializes board and special locations
	 */
	private void initEmptyBoard() {
		// pieces
		this.pieces = new ArrayList[2];
		this.pieces[0] = new ArrayList<KhetPiece>();
		this.pieces[1] = new ArrayList<KhetPiece>();

		// board
		this.board = new KhetPiece[BOARD_WIDTH][BOARD_HEIGHT];

		// special fields
		this.special = new int[BOARD_WIDTH][BOARD_HEIGHT];
		for (int i = 0; i < this.special.length; ++i)
			Arrays.fill(this.special[i], PLAYER_EMPTY);

		// silver's special places
		this.special[1][0] = PLAYER_SILVER;
		this.special[1][BOARD_HEIGHT - 1] = PLAYER_SILVER;
		for (int y = 0; y < BOARD_HEIGHT; ++y)
			this.special[BOARD_WIDTH - 1][y] = PLAYER_SILVER;

		// red's special places
		this.special[BOARD_WIDTH - 2][0] = PLAYER_RED;
		this.special[BOARD_WIDTH - 2][BOARD_HEIGHT - 1] = PLAYER_RED;
		for (int y = 0; y < BOARD_HEIGHT; ++y)
			this.special[0][y] = PLAYER_RED;
	}

	private void mirrorPieces(ArrayList<KhetPiece> oldP,
			ArrayList<KhetPiece> newP) {
		for (KhetPiece piece : oldP) {
			newP.add(piece.mirror());
		}
	}

	// ---------------------------------------------------

	public static int getOponent(int player) {
		if (PLAYER_SILVER == player)
			return PLAYER_RED;
		else if (PLAYER_RED == player)
			return PLAYER_SILVER;
		else
			throw new IllegalStateException("Illegal player ID " + player);
	}

	public static Position mirrorPos(Position pos) {
		return new Position((BOARD_WIDTH - 1) - pos.getX(), (BOARD_HEIGHT - 1)
				- pos.getY());
	}

	public static int mirrorDirection(int direction) {
		return (direction + 2) % 4;
	}

	public static int clockwiseDirection(int direction) {
		return (direction + 1) % 4;
	}

	public static int counterclockwiseDirection(int direction) {
		return (direction + 3) % 4;
	}

}
